#include <algorithm>
#include <chrono>
#include <functional>
#include <memory>
#include <sstream>
#include <sys/stat.h>
#include <utility>

#include "MPFDParser/Parser.h"
#include "http_server.hpp"
#include "lib/hash/hash_engine.hpp"
#include "lib/leveldb/cache.h"
#include "lib/msgpack.hpp"
#include "lib/re2/re2.h"
#include "lib/re2/stringpiece.h"
#include "lib/z/zlib.h"
#include "tcp_threading_server.hpp"
#include "util.hpp"
#include "version.hpp"

#define form_urlencoded_type "application/x-www-form-urlencoded"
#define form_multipart_type "multipart/form-data"

#define TEMP_DIRECTORY "temp"
#define SESSION_NAME "SESSIONID"
#define LEVELDB_PATH "mongols_leveldb"

namespace mongols
{

    int http_server::zip_level = Z_BEST_SPEED;
    size_t http_server::zip_min_size = 1024, http_server::zip_max_size = 307200; /*1kb<size<300kb*/
    std::list<std::string> http_server::zip_mime_type = {"text/html", "text/css", "text/plain", "application/x-javascript", "text/xml"};

    http_server::http_server(const std::string &host, int port, int timeout, size_t buffer_size, size_t thread_size, size_t max_body_size, int max_event_size)
        : server(0), max_body_size(max_body_size), lru_cache_size(1024), db(0), db_options(), session_expires(3600), cache_expires(3600), lru_cache_expires(300), enable_session(false), enable_cache(false), enable_lru_cache(false), openssl_is_ok(false), db_path(LEVELDB_PATH), uri_rewrite_config(), lru_cache(0), route_map()
    {
        if (thread_size > 0)
        {
            this->server = new tcp_threading_server(host, port, timeout, buffer_size, thread_size, max_event_size);
        }
        else
        {
            this->server = new tcp_server(host, port, timeout, buffer_size, max_event_size);
        }
        if (this->server)
        {
            this->db_options.create_if_missing = true;
        }
    }

    http_server::~http_server()
    {
        if (this->lru_cache)
        {
            delete this->lru_cache;
        }
        if (this->db)
        {
            delete this->db;
        }
        if (this->db_options.block_cache)
        {
            delete this->db_options.block_cache;
        }
        if (this->server)
        {
            delete this->server;
        }
    }

    void http_server::run(const std::function<bool(const mongols::request &)> &req_filter, const std::function<void(const mongols::request &, mongols::response &)> &res_filter)
    {
        tcp_server::handler_function g = std::bind(&http_server::work, this, std::cref(req_filter), std::cref(res_filter), std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5);
        if (this->enable_cache || this->enable_session)
        {
            leveldb::DB::Open(this->db_options, this->db_path, &this->db);
        }
        if (this->enable_lru_cache)
        {
            this->lru_cache = new lru11::Cache<std::string, std::shared_ptr<cache_t>>(this->lru_cache_size, 0);
        }
        this->server->run(g);
    }

    void http_server::run_with_route(const std::function<bool(const mongols::request &)> &req_filter)
    {
        auto res_filter = [&](const mongols::request &req, mongols::response &res) {
            std::vector<std::string> param;
            auto f = [&](const std::string &m) {
                return m == req.method;
            };
            for (auto &i : this->route_map)
            {
                if (std::find_if(i.method.begin(), i.method.end(), f) != i.method.end() && mongols::regex_find(*i.re2_engine, req.uri, param))
                {
                    i.handler(req, res, param);
                    break;
                }
            }
        };
        this->run(req_filter, res_filter);
    }

    std::string http_server::create_response(mongols::response &res, bool b)
    {
        std::string output;
        output.append("HTTP/1.1 ").append(std::to_string(res.status)).append(" ").append(this->get_status_text(res.status)).append("\r\n");
        res.headers.insert(std::move(std::make_pair("Server", mongols_http_server_version)));
        if (b == KEEPALIVE_CONNECTION)
        {
            res.headers.insert(std::move(std::make_pair("Connection", "keep-alive")));
        }
        else
        {
            res.headers.insert(std::move(std::make_pair("Connection", "close")));
        }
        res.headers.insert(std::move(std::make_pair("Content-Length", std::to_string(res.content.size()))));
        for (auto &i : res.headers)
        {
            output.append(i.first).append(": ").append(i.second).append("\r\n");
        }
        output.append("\r\n").append(res.content);
        return output;
    }

    bool http_server::deflate_compress(std::string &content)
    {
        if (content.empty())
        {
            return false;
        }
        const char *src = content.c_str();
        const int src_size = content.size();
        size_t max_dst_size = compressBound(src_size);
        char compressed_data[max_dst_size];
        if (compress((Bytef *)compressed_data, &max_dst_size, (const Bytef *)src, src_size) == Z_OK)
        {
            content.assign(compressed_data, max_dst_size);
            return true;
        }
        return false;
    }

    bool http_server::gzip_compress(std::string &content)
    {

        const char *src = content.c_str();
        const int src_size = content.size();
        char out[src_size];
        z_stream strm;
        strm.zalloc = Z_NULL;
        strm.zfree = Z_NULL;
        strm.opaque = Z_NULL;
        if (deflateInit2(&strm, http_server::zip_level, Z_DEFLATED, MAX_WBITS + 16, 8, Z_DEFAULT_STRATEGY) != Z_OK)
        {
            return false;
        }
        strm.next_in = (Bytef *)src;
        strm.avail_in = src_size;
        int have = 0, total = 0;
        do
        {
            strm.avail_out = src_size;
            strm.next_out = (Bytef *)out;
            if (::deflate(&strm, Z_FINISH) == Z_STREAM_ERROR)
            {
                return false;
            }
            have = src_size - strm.avail_out;
            total += have;
        } while (strm.avail_out == 0);
        if (deflateEnd(&strm) != Z_OK)
        {
            return false;
        }
        content.assign(out, total);
        return true;
    }

    std::string http_server::get_status_text(int status)
    {
        switch (status)
        {
        case 100:
            return "Continue";
        case 101:
            return "Switching Protocols";
        case 200:
            return "OK";
        case 201:
            return "Created";
        case 202:
            return "Accepted";
        case 203:
            return "Non-Authoritative Information";
        case 204:
            return "No Content";
        case 205:
            return "Reset Content";
        case 206:
            return "Partial Content";
        case 300:
            return "Multiple Choices";
        case 301:
            return "Moved Permanently";
        case 302:
            return "Found";
        case 303:
            return "See Other";
        case 304:
            return "Not Modified";
        case 305:
            return "Use Proxy";
        case 306:
            return "Switch Proxy";
        case 307:
            return "Temporary Redirect";
        case 400:
            return "Bad Request";
        case 401:
            return "Unauthorized";
        case 402:
            return "Payment Required";
        case 403:
            return "Forbidden";
        case 404:
            return "Not Found";
        case 405:
            return "Method Not Allowed";
        case 406:
            return "Not Acceptable";
        case 407:
            return "Proxy Authentication Required";
        case 408:
            return "Request Timeout";
        case 409:
            return "Conflict";
        case 410:
            return "Gone";
        case 411:
            return "Length Required";
        case 412:
            return "Precondition Failed";
        case 413:
            return "Request Entity Too Large";
        case 414:
            return "Request-URI Too Long";
        case 415:
            return "Unsupported Media Type";
        case 416:
            return "Requested Range Not Satisfiable";
        case 417:
            return "Expectation Failed";
        case 500:
            return "Internal Server Error";
        case 501:
            return "Not Implemented";
        case 502:
            return "Bad Gateway";
        case 503:
            return "Service Unavailable";
        case 504:
            return "Gateway Timeout";
        case 505:
            return "HTTP Version Not Supported";
        default:
            return ("Not supported status code.");
        }
    }

    std::string http_server::tolower(std::string &str)
    {
        std::transform(str.begin(), str.end(), str.begin(), [](unsigned char c) {
            return std::tolower(c);
        });
        return str;
    }

    void http_server::upload(mongols::request &req, const std::string &body)
    {
        try
        {
            if ((is_dir(TEMP_DIRECTORY) || mkdir(TEMP_DIRECTORY, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) == 0))
            {
                std::shared_ptr<MPFD::Parser> POSTParser(new MPFD::Parser());
                POSTParser->SetTempDirForFileUpload(TEMP_DIRECTORY);
                POSTParser->SetUploadedFilesStorage(MPFD::Parser::StoreUploadedFilesInFilesystem);
                POSTParser->SetMaxCollectedDataLength(this->server->get_buffer_size());
                POSTParser->SetContentType(req.headers["Content-Type"]);
                POSTParser->AcceptSomeData(body.c_str(), body.size());
                auto fields = POSTParser->GetFieldsMap();

                for (auto &item : fields)
                {
                    if (item.second->GetType() == MPFD::Field::TextType)
                    {
                        req.form.insert(std::make_pair(item.first, item.second->GetTextTypeContent()));
                    }
                    else
                    {
                        std::string upload_file_name = item.second->GetFileName(), ext;
                        std::string::size_type p = upload_file_name.find_last_of(".");
                        if (p != std::string::npos)
                        {
                            ext = std::move(upload_file_name.substr(p));
                        }
                        std::string temp_file = std::move(TEMP_DIRECTORY + ("/" + mongols::random_string(req.client + item.second->GetFileName()).append(ext)));
                        rename(item.second->GetTempFileName().c_str(), temp_file.c_str());
                        req.form.insert(std::make_pair(item.first, temp_file));
                    }
                }
            }
        }
        catch (MPFD::Exception &err)
        {
        }
    }

    std::string http_server::work(
        const std::function<bool(const mongols::request &)> &req_filter, const std::function<void(const mongols::request &, mongols::response &)> &res_filter, const std::pair<char *, size_t> &input, bool &keepalive, bool &send_to_other, tcp_server::client_t &client, tcp_server::filter_handler_function &)
    {
        send_to_other = false;
        mongols::request req;
        mongols::response res;
        mongols::http_request_parser parser(req);
        bool enable_zip = false;
        http_server::zip_t zip_type = http_server::zip_t::unknown;
        if (parser.parse(input.first, input.second))
        {
            client.type = tcp_server::connection_t::HTTP;
            std::unordered_map<std::string, std::string>::const_iterator tmp_iterator;
            std::string &body = parser.get_body();
            req.client = client.ip;
            if ((tmp_iterator = req.headers.find("User-Agent")) != req.headers.end())
            {
                req.user_agent = tmp_iterator->second;
            }
            if (body.size() > this->max_body_size)
            {
                body.clear();
                res.content = std::move("Not allowed to upload this resource.");
                res.status = 500;
                keepalive = CLOSE_CONNECTION;
                return this->create_response(res, keepalive);
            }
            if (req_filter(req))
            {
                for (auto &rewrite_pattern : this->uri_rewrite_config)
                {
                    if (RE2::Replace(&req.uri, rewrite_pattern.first, rewrite_pattern.second))
                    {
                        break;
                    }
                }

                if (parser.keep_alive())
                {
                    keepalive = KEEPALIVE_CONNECTION;
                }

                if ((tmp_iterator = req.headers.find("Accept-Encoding")) != req.headers.end())
                {
                    if (tmp_iterator->second.find("gzip") != std::string::npos)
                    {
                        zip_type = zip_t::gzip;
                        enable_zip = true;
                    }
                    else if (tmp_iterator->second.find("deflate") != std::string::npos)
                    {
                        zip_type = zip_t::deflate;
                        enable_zip = true;
                    }
                }

                if ((tmp_iterator = req.headers.find("If-Modified-Since")) != req.headers.end() && difftime(time(0), mongols::parse_http_time((u_char *)tmp_iterator->second.c_str(), tmp_iterator->second.size())) <= this->lru_cache_expires)
                {
                    res.status = 304;
                    res.content.clear();
                    return this->create_response(res, keepalive);
                }

                std::string cache_k = std::move(hash_engine::md5(req.method + req.uri + "?" + req.param));
                if (this->enable_lru_cache && req.method == "GET")
                {
                    if (this->lru_cache->contains(cache_k))
                    {
                        auto cache_ele = this->lru_cache->get(cache_k);
                        if (cache_ele->expired(this->lru_cache_expires))
                        {
                            this->lru_cache->remove(cache_k);
                        }
                        else
                        {
                            res.status = cache_ele->status;
                            res.content = cache_ele->content;
                            res.headers.find("Content-Type")->second = cache_ele->content_type;
                            res.headers.insert(std::move(std::make_pair("Last-Modified", mongols::http_time(&cache_ele->t))));
                            if (cache_ele->enable_zip)
                            {
                                if (cache_ele->zip_type == zip_t::deflate)
                                {
                                    res.headers.insert(std::move(std::make_pair("Content-Encoding", "deflate")));
                                }
                                else if (cache_ele->zip_type == zip_t::gzip)
                                {
                                    res.headers.insert(std::move(std::make_pair("Content-Encoding", "gzip")));
                                }
                            }
                            return this->create_response(res, keepalive);
                        }
                    }
                }

                if (!req.param.empty())
                {
                    mongols::parse_param(req.param, req.form);
                }

                std::string session_val, cache_v;

                if (this->db)
                {
                    if (this->enable_session)
                    {
                        if ((tmp_iterator = req.headers.find("Cookie")) != req.headers.end())
                        {
                            mongols::parse_param(tmp_iterator->second, req.cookies, ';');
                            if ((tmp_iterator = req.cookies.find(SESSION_NAME)) != req.cookies.end())
                            {
                                session_val = tmp_iterator->second;
                                std::string v;
                                if (this->db->Get(leveldb::ReadOptions(), tmp_iterator->second, &v).ok())
                                {
                                    this->deserialize(v, req.session);
                                }
                                else
                                {
                                    this->db->Put(leveldb::WriteOptions(), tmp_iterator->second, this->serialize(req.session));
                                }
                            }
                        }
                        else
                        {
                            std::chrono::system_clock::time_point now_time = std::chrono::system_clock::now();
                            std::time_t expire_time = std::chrono::system_clock::to_time_t(now_time + std::chrono::seconds(this->session_expires));
                            std::string session_cookie;
                            session_cookie.append(SESSION_NAME).append("=").append(mongols::random_string("")).append("; HttpOnly; Path=/; Expires=").append(mongols::http_time(&expire_time));
                            if (this->openssl_is_ok)
                            {
                                session_cookie.append("; Secure");
                            }
                            res.headers.insert(std::move(std::make_pair("Set-Cookie", session_cookie)));
                        }
                    }

                    if (this->enable_cache)
                    {
                        if (this->db->Get(leveldb::ReadOptions(), cache_k, &cache_v).ok())
                        {
                            this->deserialize(cache_v, req.cache);
                        }
                        else
                        {
                            this->db->Put(leveldb::WriteOptions(), cache_k, this->serialize(req.cache));
                        }
                    }
                }

                if (!body.empty() && (tmp_iterator = req.headers.find("Content-Type")) != req.headers.end())
                {
                    if (tmp_iterator->second.find(form_multipart_type) != std::string::npos)
                    {
                        this->upload(req, body);
                        body.clear();
                    }
                    else if (tmp_iterator->second.find(form_urlencoded_type) != std::string::npos)
                    {
                        mongols::parse_param(body, req.form);
                    }
                    else
                    {
                        req.form["__body__"] = std::move(body);
                    }
                }

                res_filter(req, res);
                if (res.status >= 400)
                {
                    keepalive = CLOSE_CONNECTION;
                }

                if (enable_zip)
                {
                    size_t len = res.content.size();
                    if ((len > http_server::zip_min_size && len <= http_server::zip_max_size))
                    {
                        const std::string &res_content_type = res.headers.find("Content-Type")->second;
                        auto zip_mime_type_iter = std::find_if(http_server::zip_mime_type.begin(), http_server::zip_mime_type.end(), [&](const std::string &item) {
                            return res_content_type.find(item) != std::string::npos;
                        });
                        if (zip_mime_type_iter == http_server::zip_mime_type.end())
                        {
                            goto zip_error;
                        }
                        if (zip_type == zip_t::deflate)
                        {
                            if (this->deflate_compress(res.content))
                            {
                                res.headers.insert(std::move(std::make_pair("Content-Encoding", "deflate")));
                            }
                            else
                            {
                                goto zip_error;
                            }
                        }
                        else if (zip_type == zip_t::gzip)
                        {
                            if (this->gzip_compress(res.content))
                            {
                                res.headers.insert(std::move(std::make_pair("Content-Encoding", "gzip")));
                            }
                            else
                            {
                                goto zip_error;
                            }
                        }
                        else
                        {
                            goto zip_error;
                        }
                    }
                    else
                    {
                    zip_error:
                        enable_zip = false;
                        zip_type = http_server::zip_t::unknown;
                    }
                }

                std::unordered_map<std::string, std::string> *ptr = 0;
                if (!res.session.empty() && this->db)
                {

                    if (!req.session.empty())
                    {
                        for (auto &i : res.session)
                        {
                            req.session[i.first] = std::move(i.second);
                        }
                        ptr = &req.session;
                    }
                    else
                    {
                        ptr = &res.session;
                    }
                    this->db->Put(leveldb::WriteOptions(), session_val, this->serialize(*ptr));
                }

                if (!res.cache.empty() && this->db)
                {
                    if (!req.cache.empty())
                    {
                        for (auto &i : res.cache)
                        {
                            req.cache[i.first] = std::move(i.second);
                        }
                        ptr = &req.cache;
                    }
                    else
                    {
                        ptr = &res.cache;
                    }
                    this->db->Put(leveldb::WriteOptions(), cache_k, this->serialize(*ptr));
                }
                if (this->enable_lru_cache && this->lru_cache_expires > 0 && req.method == "GET" && res.status == 200)
                {
                    std::shared_ptr<cache_t> cache_ele = std::make_shared<cache_t>();
                    cache_ele->content = res.content;
                    cache_ele->status = res.status;
                    cache_ele->content_type = res.headers.find("Content-Type")->second;
                    cache_ele->enable_zip = enable_zip;
                    cache_ele->zip_type = zip_type;
                    this->lru_cache->insert(cache_k, cache_ele);
                    res.headers.insert(std::move(std::make_pair("Last-Modified", mongols::http_time(&cache_ele->t))));
                }
            }
        }

        return this->create_response(res, keepalive);
    }

    void http_server::set_session_expires(long long expires)
    {
        this->session_expires = expires;
    }

    void http_server::set_cache_expires(long long expires)
    {
        this->cache_expires = expires;
    }

    void http_server::set_enable_cache(bool b)
    {
        this->enable_cache = b;
    }

    void http_server::set_enable_session(bool b)
    {
        this->enable_session = b;
    }

    void http_server::set_max_file_size(size_t len)
    {
        this->db_options.max_file_size = len;
    }

    void http_server::set_max_open_files(int len)
    {
        this->db_options.max_open_files = len;
    }

    void http_server::set_write_buffer_size(size_t len)
    {
        this->db_options.write_buffer_size = len;
    }

    void http_server::set_cache_size(size_t len)
    {
        this->db_options.block_cache = leveldb::NewLRUCache(len);
    }

    void http_server::set_enable_compression(bool b)
    {
        if (!b)
        {
            this->db_options.compression = leveldb::kNoCompression;
        }
    }

    void http_server::set_db_path(const std::string &path)
    {
        this->db_path = path;
    }

    void http_server::set_uri_rewrite(const std::pair<std::string, std::string> &p)
    {
        this->uri_rewrite_config.push_back(p);
    }

    void http_server::set_enable_lru_cache(bool b)
    {
        this->enable_lru_cache = b;
    }

    void http_server::set_lru_cache_expires(long long expires)
    {
        this->lru_cache_expires = expires;
    }

    void http_server::set_lru_cache_size(size_t len)
    {
        this->lru_cache_size = len;
    }

    bool http_server::set_openssl(const std::string &crt_file, const std::string &key_file, openssl::version_t v, const std::string &ciphers, long flags)
    {
        this->openssl_is_ok = this->server->set_openssl(crt_file, key_file, v, ciphers, flags);
        return this->openssl_is_ok;
    }

    void http_server::set_enable_blacklist(bool b)
    {
        this->server->set_enable_blacklist(b);
    }

    void http_server::set_enable_security_check(bool b)
    {
        this->server->set_enable_security_check(b);
    }

    void http_server::set_enable_whitelist(bool b)
    {
        this->server->set_enable_whitelist(b);
    }
    void http_server::set_whitelist(const std::string &ip)
    {
        this->server->set_whitelist(ip);
    }
    void http_server::del_whitelist(const std::string &ip)
    {
        this->server->del_whitelist(ip);
    }

    void http_server::set_whitelist_file(const std::string &path)
    {
        this->server->set_whitelist_file(path);
    }

    void http_server::set_shutdown(const tcp_server::shutdown_function &f)
    {
        this->server->set_shutdown(f);
    }

    std::string http_server::serialize(const std::unordered_map<std::string, std::string> &m)
    {
        std::stringstream ss;
        msgpack::pack(ss, m);
        ss.seekg(0);
        return ss.str();
    }

    void http_server::deserialize(const std::string &str, std::unordered_map<std::string, std::string> &m)
    {
        msgpack::unpack(str.c_str(), str.size()).get().convert(m);
    }

    http_server::cache_t::cache_t()
        : status(200), t(time(0)), content_type(), content(), enable_zip(false), zip_type(http_server::zip_t::unknown)
    {
    }

    bool http_server::cache_t::expired(long long expires) const
    {
        return difftime(time(0), this->t) > expires;
    }

    void http_server::add_route(const std::list<std::string> &method, const std::string &pattern, const std::function<void(const mongols::request &, mongols::response &, const std::vector<std::string> &)> &hander)
    {
        route_t r;
        r.method = method;
        r.pattern = pattern;
        r.handler = hander;
        r.re2_options = std::move(std::make_shared<RE2::Options>());
        r.re2_options->set_log_errors(false);
        r.re2_engine = std::move(std::make_shared<RE2>("(" + pattern + ")", *r.re2_options));
        this->route_map.emplace_back(std::move(r));
    }
} // namespace mongols
